package gin

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"net"
	"net/http"
	"os"
	"runtime"
	"strings"
	"time"

	"gitee.com/zawei/yggdrasil/pkg/log"
	"gitee.com/zawei/yggdrasil/pkg/metrics"
	"gitee.com/zawei/yggdrasil/pkg/trace"
	"gitee.com/zawei/yggdrasil/pkg/transport/server"
	"gitee.com/zawei/yggdrasil/pkg/utils/xstrings"
	"github.com/gin-gonic/gin"
	"github.com/opentracing/opentracing-go"
	"github.com/opentracing/opentracing-go/ext"
)

var (
	dunno     = []byte("???")
	centerDot = []byte("·")
	dot       = []byte(".")
	slash     = []byte("/")
)

func extractAID(ctx *gin.Context) string {
	return ctx.Request.Header.Get("AID")
}

func recoverMiddleware(slowQueryThreshold time.Duration) gin.HandlerFunc {
	return func(c *gin.Context) {
		var beg = time.Now()
		var fields = make([]interface{}, 0, 16)
		var brokenPipe bool
		defer func() {
			//Latency
			fields = append(fields, "cost", time.Since(beg).Seconds())
			if rec := recover(); rec != nil {
				if ne, ok := rec.(*net.OpError); ok {
					if se, ok := ne.Err.(*os.SyscallError); ok {
						if strings.Contains(strings.ToLower(se.Error()), "broken pipe") || strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") {
							brokenPipe = true
						}
					}
				}
				var err = rec.(error)
				fields = append(fields, "stack", xstrings.Bytes2str(stack(3)))
				fields = append(fields, "error", err)
				log.Errorw("recovery from panic", fields...)
				// If the connection is dead, we can't write a status to it.
				if brokenPipe {
					c.Error(err) // nolint: errcheck
					c.Abort()
					return
				}
				c.AbortWithStatus(http.StatusInternalServerError)
				return
			}
			fields = append(fields,
				"method", c.Request.Method,
				"status", c.Writer.Status(),
				"size", c.Writer.Size(),
				"host", c.Request.Host,
				"path", c.Request.URL.Path,
				"ip", c.ClientIP())
			if code := c.Writer.Header().Get("x-yggdrasil-reason"); code != "" {
				fields = append(fields, "reason", code)
			}
			if in, ok := c.Get("req"); ok {
				data, _ := json.Marshal(in)
				fields = append(fields, "req", string(data))
			}
			if out, ok := c.Get("ou"); ok {
				fields = append(fields, "out", out)
			}
			errs := c.Errors.ByType(gin.ErrorTypePrivate)
			if len(errs) > 0 {
				for _, err := range errs {
					fields = append(fields, "error", err.Err)
				}
				log.Errorw("access", fields...)
			} else {
				if slowQueryThreshold > 0 && time.Since(beg) > slowQueryThreshold {
					log.Warnw("access", fields...)
				} else {
					log.Infow("access", fields...)
				}

			}
		}()
		c.Next()
	}
}

// stack returns a nicely formatted stack frame, skipping skip frames.
func stack(skip int) []byte {
	buf := new(bytes.Buffer) // the returned data
	// As we loop, we open files and read them. These variables record the currently
	// loaded file.
	var lines [][]byte
	var lastFile string
	for i := skip; ; i++ { // Skip the expected number of frames
		pc, file, line, ok := runtime.Caller(i)
		if !ok {
			break
		}
		// Print this much at least.  If we can't find the source, it won't show.
		fmt.Fprintf(buf, "%s:%d (0x%x)\n", file, line, pc)
		if file != lastFile {
			data, err := ioutil.ReadFile(file)
			if err != nil {
				continue
			}
			lines = bytes.Split(data, []byte{'\n'})
			lastFile = file
		}
		fmt.Fprintf(buf, "\t%s: %s\n", function(pc), source(lines, line))
	}
	return buf.Bytes()
}

// source returns a space-trimmed slice of the n'th line.
func source(lines [][]byte, n int) []byte {
	n-- // in stack trace, lines are 1-indexed but our array is 0-indexed
	if n < 0 || n >= len(lines) {
		return dunno
	}
	return bytes.TrimSpace(lines[n])
}

// function returns, if possible, the name of the function containing the PC.
func function(pc uintptr) []byte {
	fn := runtime.FuncForPC(pc)
	if fn == nil {
		return dunno
	}
	name := []byte(fn.Name())
	// The name includes the path name to the package, which is unnecessary
	// since the file name is already included.  Plus, it has center dots.
	// That is, we see
	//	runtime/debug.*T·ptrmethod
	// and want
	//	*T.ptrmethod
	// Also the package path might contains dot (e.g. code.google.com/...),
	// so first eliminate the path prefix
	if lastSlash := bytes.LastIndex(name, slash); lastSlash >= 0 {
		name = name[lastSlash+1:]
	}
	if period := bytes.Index(name, dot); period >= 0 {
		name = name[period+1:]
	}
	name = bytes.Replace(name, centerDot, dot, -1)
	return name
}

func timeFormat(t time.Time) string {
	timeString := t.Format("2006/01/02 - 15:04:05")
	return timeString
}

func metricServerInterceptor() gin.HandlerFunc {
	return func(c *gin.Context) {
		beg := time.Now()
		c.Next()
		_ = metrics.HistogramObserve(server.MetricsHandleSec, time.Since(beg).Seconds(), map[string]string{
			"type": "http_gin", "method": c.Request.Method + "." + c.Request.URL.Path, "peer": extractAID(c),
		})
		_ = metrics.CounterAdd(server.MetricsHandleTotal, 1, map[string]string{
			"type": "http_gin", "method": c.Request.Method + "." + c.Request.URL.Path, "peer": extractAID(c), "code": http.StatusText(c.Writer.Status()),
		})
		return
	}
}

func extractRpcServerOpt(header map[string][]string) opentracing.StartSpanOption {
	sc, err := opentracing.GlobalTracer().Extract(opentracing.HTTPHeaders, trace.TextArrayMapCarrier(header))
	if err != nil {
		return trace.NilStartSpanOption{}
	}
	return opentracing.ChildOf(sc)
}

func traceServerInterceptor() gin.HandlerFunc {
	return func(c *gin.Context) {
		span, ctx := opentracing.StartSpanFromContext(
			c.Request.Context(),
			c.Request.Method+" "+c.Request.URL.Path,
			extractRpcServerOpt(c.Request.Header),
		)
		ext.SpanKind.Set(span, "server")
		ext.Component.Set(span, "http")
		ext.HTTPUrl.Set(span, c.Request.URL.Path)
		ext.HTTPMethod.Set(span, c.Request.Method)
		ext.PeerHostIPv4.SetString(span, c.ClientIP())
		c.Request = c.Request.WithContext(ctx)
		defer span.Finish()
		c.Next()
	}
}
